package com.sk.ccflower.server.lib.sockUtils.nio;

import java.io.IOException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.ClosedSelectorException;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.Set;

public class NioSelector
{
    private boolean m_bEndSelect = false;
    private final Selector m_selector;

    public NioSelector() throws Exception
    {
        m_selector = Selector.open();
    }

    /** Selector 및 SelectionKey의 모든 channel을 close 한다.
     */
    public void close()
    {
        if(!m_selector.isOpen()) return;

        try
        {
            SelectionKey key;
            Iterator<SelectionKey> itr;

            itr = m_selector.keys().iterator();

            while(itr.hasNext())
            {
                key = (SelectionKey)itr.next();
                try {
                    key.channel().close();
                } catch (IOException e) {
                }
            }

            try {
                m_selector.close();
            } catch (IOException e) {
            }
        }
        catch (Exception e)
        {
        }
    }

    /**
     * 입력된 채널에서 입력된 timeout이내에 input이 흘러 들어오는지 체크하는 함수.
     * @param channel: select할 채널
     * @param timeout: milliseconds
     * @return
     */
    public static boolean isSelected(SelectableChannel channel, long timeout) throws Exception
    {
        boolean isSelected = false;
        Selector selector = null;

        try {
            selector = Selector.open();
            isSelected = isSelected(selector, channel, timeout);
        } catch (Exception e) {
            throw e;
        } finally {
            try {
                if(selector!=null) selector.close();
            } catch (Exception e) {
            }
        }

        return isSelected;
    }

    /**
     * 입력된 채널에서 입력된 timeout이내에 input이 흘러 들어오는지 체크하는 함수.
     * @param selector: 채널을 select할 Selector
     * @param channel: select할 채널
     * @param timeout: milliseconds
     * @return
     */
    public static boolean isSelected(Selector selector, SelectableChannel channel, long timeout) throws Exception
    {
        if(timeout<=0) return false;

        boolean isSelected = false;
        SelectionKey key = null;
        SelectionKey myKey = null;
        Iterator<SelectionKey> itr;

        try {
            try {
                itr = selector.keys().iterator();

                while(itr.hasNext()) {
                    key = (SelectionKey)itr.next();
                    key.cancel();
                }
            } catch (Exception e) {
            }

            myKey = channel.register(selector, SelectionKey.OP_READ);

            selector.select(timeout);
            itr = selector.selectedKeys().iterator();

            while(itr.hasNext())
            {
                key = (SelectionKey)itr.next();
                if(!key.isValid()) continue;

                if(key.channel().equals(channel)) {
                    isSelected = true;
                    break;
                }
            }
        } catch (Exception e) {
            throw new Exception("isSelected exception: " + e.getMessage(), e);
        } finally {
            try {
                if(myKey!=null) {
                    myKey.cancel();
                    selector.select(1);
                }
            } catch (Exception e) {
            }
        }

        return isSelected;
    }

    /**
     * select를 마치고 run()을 빠져 나옴
     */
    public void endSelect()
    {
        m_bEndSelect = true;
    }

    /**
     * select를 수행하고 있는 Selector를 리턴해 주는 함수
     * @return Selector
     */
    public Selector getSelector()
    {
        return m_selector;
    }

    /**
     * Selector에 존재하는 모든 SelectionKey를 리턴해 주는 함수
     * @return 현재 Select하고 있는 모든 SelectionKey Set
     */
    public Set<SelectionKey> getKeys() throws ClosedSelectorException
    {
        return m_selector.keys();
    }

    /**
     * NioHandler와 Channel을 Selector에 등록하는 함수, 등록하고 나면 Select 리스트에 포함되어 select를 수행하게 되고
     * Channel에서 input이 들어오면 NioHandler의 handleInput()이 호출된다.
     * @param handle: channel에서 input 들어올 경우 handleInput이 호출되는 Handler
     * @param channel: Select해야하는 채널
     * @param ops: SelectionKey의 option
     * @throws ClosedChannelException
     * @throws Exception
     */
    public void registerHandler(NioHandler handle, SelectableChannel channel, int ops) throws ClosedChannelException, Exception
    {
        try {
            SelectionKey key = channel.register(m_selector, ops);
            key.attach(handle);
        } catch (ClosedChannelException e) {
        	e.printStackTrace();
            throw new Exception("registerHandler exception: " + e.getMessage(), e);
        } catch (Exception e) {
        	e.printStackTrace();
            throw new Exception("registerHandler exception: " + e.getMessage(), e);
        }
    }

    /**
     * Select를 계속적으로 select하면서 input이 들어오면 등록되어 있는 해당 NioHandler의 handleInput()을 호출한다.
     * @throws Exception
     */
    public void run() throws Exception
    {
        SelectionKey key;
        Iterator<SelectionKey> itr;
        NioHandler handle = null;
        int numKeys;
        long currentTime;
        long currentTimeMS;

        try
        {
            while(true)
            {
                numKeys = m_selector.select(1*1000);

                if(numKeys>0)
                {
                    itr = m_selector.selectedKeys().iterator();

                    while(itr.hasNext())
                    {
                        try {
                            key = (SelectionKey)itr.next();
                        } catch (ConcurrentModificationException e) {
                            break;
                        }
                        handle = (NioHandler)key.attachment();

                        if(handle==null) {
                            try {
                                key.channel().close();
                            } catch (IOException e) {
                            }
                        }
                        else {
                            handle.handleInput(key);
                            if(m_bEndSelect) break;
                        }

                        itr.remove();
                    }
                }
                if(m_bEndSelect) break;

                itr = m_selector.keys().iterator();

                while(itr.hasNext())
                {
                    try {
                        key = (SelectionKey)itr.next();
                    } catch (ConcurrentModificationException e) {
                        break;
                    }
                    if(!key.isValid()) continue;

                    handle = (NioHandler)key.attachment();

                    if(handle==null)
                    {
                        try {
                            key.channel().close();
                        } catch (IOException e) {
                        }
                    }
                    else if(handle.getTimeout()>0)
                    {
                        currentTimeMS = System.currentTimeMillis();
                        currentTime = currentTimeMS/1000;

                        if(handle.getNextTimeout()<currentTime)
                        {
                            handle.setNexTimeout(currentTime);
                            handle.handleTimeout(key, handle.getDate(currentTimeMS));
                            if(m_bEndSelect) break;
                        }
                    }
                }
                if(m_bEndSelect) break;
            }
        }
        catch (Exception e)
        {
        	e.printStackTrace();
            throw new Exception(getClass().getSimpleName() + " run exception: " + e.getMessage(), e);
        }
        finally
        {
            close();
        }
    }
}
